
const { SyncHook } = require("tapable")
const path = require("path");
const fs = require("fs");
const prettier = require("prettier");
const parser = require('@babel/parser')
let types = require("@babel/types");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
function toUnixPath(filePath) {
  return filePath.replace(/\\/g, "/");
}
//获取文件路径
function tryExtensions(modulePath, extensions) {
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (let i = 0; i < extensions.length; i++) {
    let filePath = modulePath + extensions[i];
    if (fs.existsSync(filePath)) {
      return filePath;
    }
  }
  throw new Error(`${modulePath} is not exist`);
}

function getSource(chunk) {
  return `
    (()=>{
      var modules = {
        ${chunk.modules.map(module => {
    return `"${module.id}":(module) => {
            ${module.sourceCode}
          }`
  })}
      };
      
      var cache = {};
      function require(moduleId){
        var cachedModule = cache[moduleId];
        if(cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = (cache[moduleId] = {
          exports:{},
        });
        modules[moduleId](module,module.exports,require);

        return module.exports;
      };

      var exports = {};
      ${chunk.entryModule.sourceCode}

    })()
  `
}

const baseDir = toUnixPath(process.cwd());
class Compiler {
  constructor(webpackConfig) {
    this.options = webpackConfig;
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook()
    }
  }
  compiler(callback) {
    let compilation = new Compilation(this.options);
    compilation.build(callback)
  }
  run(callback) {
    //开始钩子
    this.hooks.run.call();
    const onCompiled = (err, stats, fileDependencies) => {
      //第十步：确定好输出内容之后，根据配置的输出路径和文件名，将文件内容写入到文件系统（这里就是硬盘）
      for (let filename in stats.assets) {
        let filePath = path.join(this.options.output.path, filename) //拼接出文件的绝对路径
        if (!fs.existsSync(this.options.output.path)) {
          fs.mkdirSync(this.options.output.path, { recursive: true })
        }
        //输出内容格式化
        prettier.format(stats.assets[filename], { parser: "babel" }).then((result) => {
          //将编译好的文件内容写入到硬盘上
          fs.writeFileSync(filePath, result, 'utf8');
        })
      }
      callback(err, {
        toJson:() => stats
      })
      fileDependencies.forEach(filePath => {
        fs.watchFile(filePath, { interval: 1000 }, (curr, prev) => {
          //如果文件内容发生变化，重新编译
          this.compiler(onCompiled);
        })
      })

      //结束钩子
      this.hooks.done.call();
    }
    //执行编译函数
    this.compiler(onCompiled);
  }
}


class Compilation {
  constructor(webpackConfig) {
    this.options = webpackConfig;
    this.modules = [];
    this.chunks = [];
    this.assets = {};
    this.fileDependencies = [];
  }
  build(callback) {
    //获取入口文件
    let { entry } = this.options;
    if (typeof entry === 'string') {
      entry = { main: entry } //如果是单入口，将entry:"xx"变成{main:"xx"}，这里需要做兼容
    }
    for (let entryName in entry) {
      let entryPath = entry[entryName];
      let entryFilePath = path.posix.join(baseDir, entryPath)
      this.fileDependencies.push(entryFilePath) //将入口文件路径添加到依赖列表中
      let entryModule = this.buildModule(entryName, entryFilePath) //编译入口文件
      this.modules.push(entryModule)
      //第八步：等所有模块都编译完成后，根据模块之间的依赖关系，组装代码块 `chunk`（一般来说，每个入口文件会对应一个代码块`chunk`，每个代码块`chunk`里面会放着本入口模块和它依赖的模块）
      let chunk = {
        name: entryName,
        entryModule,
        modules: this.modules.filter(m => m.names.includes(entryName))
      }
      this.chunks.push(chunk)
    }
    //第九步：把各个代码块 `chunk` 转换成一个一个文件加入到输出列表
    this.chunks.forEach((chunk) => {
      const { output } = this.options
      const { filename, filePath } = output
      if (!output.filename) {
        let filename = filename.replace("[name]", chunk.name)
      }
      this.assets[filename] = getSource(chunk)
    })

    callback(null, {
      chunks: this.chunks,
      modules: this.modules,
      assets: this.assets
    }, this.fileDependencies);
  }
  buildModule(name, modulePath) {
    //1、读取源代码
    let sourceCode = fs.readFileSync(modulePath, "utf8");

    //2、获取moduleId 
    let moduleId = './' + path.posix.relative(baseDir, modulePath)
    let module = {
      id: moduleId,
      modulePath,
      names: [name],
      sourceCode,
      dependencies: []
    }

    //加载配置的loader
    let loaders = []
    let optModule = this.options.module || {}
    const { rules = [] } = optModule
    rules.forEach((rule) => {
      const { test, use } = rule
      if (module.id.match(test)) {
        loaders.push(...use)
      }
    })
    //从右向左执行loader
    sourceCode = loaders.reduceRight((acc, curLoader) => {
      return curLoader(acc)
    }, sourceCode)


    //AST转化编译
    let ast = parser.parse(sourceCode, { sourceType: "module" })
    traverse(ast, {
      CallExpression: (nodePath) => {
        const { node } = nodePath
        //在 `AST` 中查找 `require` 语句，找出依赖的模块名称和绝对路径
        if (node.callee.name === 'require') {
          //require的路径
          let requirePath = node.arguments[0].value
          //当前模块的目录路径
          let dirName = path.posix.dirname(modulePath)
          // 依赖模块的绝对路径
          let depModulePath = path.posix.join(dirName, requirePath)
          //获取配置中的extensions
          let extensions = this.options.resolve?.extensions || ['.js'];
          //给依赖的模块添加后缀名，找到一个真实在硬盘上存在的文件
          depModulePath = tryExtensions(depModulePath, extensions)
          //将依赖的模块路径添加到this.fileDependencies中
          this.fileDependencies.push(depModulePath)
          //生成依赖模块的模块id
          let moduleId = './' + path.posix.relative(baseDir, depModulePath)
          // 修改ast结构，把依赖的模块改为依赖`模块 id` require("./name")=>require("./src/name.js")
          node.arguments = [types.stringLiteral(moduleId)]
          module.dependencies.push({ moduleId, depModulePath })
        }
      }
    })

    //把修改后的ast重新生成未code
    let { code } = generator(ast)
    module.sourceCode = code
    //对模块的dependencies进行递归编译
    module.dependencies.forEach((dep) => {
      //考虑到多入口打包 ：一个模块被多个其他模块引用，不需要重复打包
      let existModule = this.modules.find((item) => item.id === dep.moduleId)
      if (existModule) {
        existModule.name.push(name)
      } else {
        let depModule = this.buildModule(name, dep.depModulePath)
        this.modules.push(depModule);
      }
    })

    return module
  }
}

module.exports = function webpack(webpackConfig) {

  //初始化compiler
  const compiler = new Compiler(webpackConfig)

  //挂载插件
  const { plugins } = webpackConfig;
  for (const plugin of plugins) {
    plugin.apply(compiler)
  }
  return compiler
}